#pragma once

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h> // 必须在gcc编译时加上-lphread命令，才能包这个头文件
#include "Log.hpp"

#define BACKLOG 5

class TcpServer{
    private:
        int port;// 端口号
        int listen_sock;// 监听套接字，在正式建立TCP连接后，我们想给对面发消息，首先要从本地向listen_sock指向的网卡发消息
        static TcpServer *svr; // 保存创建好的单例的指针
    private:
        // 构造函数
        TcpServer(int _port):port(_port),listen_sock(-1)
        {}
        // 单例模型下，我们需要把拷贝函数给禁了
        TcpServer(const TcpServer &s){}// 也可以写成TcpServer(const TcpServer &s)=delete;
        TcpServer& operator=(const TcpServer &s)=delete; // 禁用赋值运算符重载函数
    public:
        // 获取单例
        static TcpServer *getinstance(int port)
        {  
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            // 如果此时还没有创建单例，我们就创建一个单例
            // 如果此时已经创建了单例，那我们就直接返回单例指针
            if(nullptr == svr){
                // 为了保证单例只能有一个，防止在没有单例时同时创建多个TCPserver对象，因此我们要加锁保证同一时刻只能有一个线程执行new语句
                pthread_mutex_lock(&lock);
                // 提问：这里为什么要做第二次判断？答：虽然外层的检查可以避免大部分情况下的锁竞争，但在多线程环境中，可能会出现多个线程同时通过外层检查的情况。例如，线程 A 和线程 B 同时判断 svr 为 nullptr，此时它们都会尝试进入加锁的代码块。如果没有内层的检查，线程 A 先获取到锁并创建了 svr 实例，当线程 A 释放锁后，线程 B 接着获取锁，也会再次创建 svr 实例，这就违背了单例模式的原则，导致创建了多个实例。
                if(nullptr == svr){
                    // 根据传入的端口号port创建一个TcpServer类的对象
                    svr = new TcpServer(port);
                    // 并调用这个对象的init函数完成对它的初始化
                    svr->InitServer();
                }
                pthread_mutex_unlock(&lock);
            }
            return svr;
        }
        // 对服务器进行初始化
        void InitServer()
        {
            Socket();
            Bind();
            Listen();
            LOG(INFO, "tcp_server init ... success");
        }
        // 创建监听套接字
        void Socket()
        {
            //
            //我们都知道，网卡充当着本地主机和对端主机之间通信的桥梁，网卡与本地主机之间是主机内通信，网卡与对端主机之间是网络通信
            //调用socket函数创建套接字的本质实际上就是告诉网卡来活了，让网卡新开一个业务（对应就是新增一个网卡的文件描述符），专门用来负责协调主机之间的通信
            listen_sock = socket(AF_INET, SOCK_STREAM, 0);
            // socket函数返回的其实是网卡的文件描述符，服务器想给客户端发任何消息，都得先把这消息发给网卡，然后由网卡发给对端主机
            if(listen_sock < 0){
                LOG(FATAL, "socket error!");
                exit(1);
            }

            // 调用setsockopt时设置SO_REUSEADDR 选项，以避免在服务器重启时因端口被占用而无法绑定的问题。
            int opt = 1;
            setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
                // 第一个参数指明要设置的套接字
                // 第二个参数指明选项所在的协议层。SOL_SOCKET 表示通用的套接字选项，意味着这些选项适用于所有类型的套接字，而不局限于特定的协议。
                // 第三个参数设置为SO_REUSEADDR 表示允许在绑定套接字时，重用处于 TIME_WAIT 状态的地址和端口。
                // 第四个参数&opt：若 opt 的值为 1，则表示启用SO_REUSEADDR 选项；若为 0，则表示禁用SO_REUSEADDR 选项。
                // 第五个参数sizeof(opt)：这是 setsockopt 函数的 optlen 参数，它指明第四个参数的大小，即 opt 变量的字节数。
            LOG(INFO, "create socket ... success");
        }
        // 绑定套接字
        void Bind()
        {

            struct sockaddr_in local; // 这个Local指的就是我server端开放的套接字
            memset(&local, 0, sizeof(local)); //将结构体清空，打扫干净屋子再请客
            local.sin_family = AF_INET; // AF_INET 表示使用 32 位的 IPv4 地址。
            local.sin_port = htons(port); // 这个选项是用来设置server端开放的端口号的，开放之后后续其他客户端就可以来连了
            // port是TcpServer的成员变量，追根溯源，就是我们在命令行中输入./httpserver 8081中传入的8081
            local.sin_addr.s_addr = INADDR_ANY; // Local指的是server端开放的套接字， 那local.sin_addr.s_addr 自然也就是server端的IP地址
            //但是云服务器不能直接绑定公网IP，我们的解决方案就是将其设置为INADDR_ANY，让它随机绑定

            // 我们都知道TCP协议通信之间要先建立连接，也就是要建立server端的套接字与client端的套接字之间的链接
            // bind的本质就是建立server套接字（local）与网卡（listen_sock）之间的连接
            // 至于client端的套接字与网卡之间的连接与数据传输你不用操心，自有双方的操作系统内核配合完成
            if(bind(listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
                LOG(FATAL, "bind error!");
                exit(2);
            }
            LOG(INFO, "bind socket ... success");
        }
        void Listen()
        {
            // 让套接字处于监听状态，其能够监听到来自外界客户端的连接请求
            if(listen(listen_sock, BACKLOG) < 0){
                // BACKLOG指定请求队列的最大长度
                LOG(FATAL, "listen socket error!");
                exit(3);
            }
            LOG(INFO, "listen socket ... success");
        }
        
        // 这个函数专门用来返回监听套接字对应的文件描述符
        int Sock()
        {
            return listen_sock;
        }
        ~TcpServer()
        {
            // listen_sock >= 0说明监听套接字创建成功，那我就在析构的时候关闭套接字
            // listen_sock < 0说明监听套接字压根就没有创建成功，既然都没成功创建，那我就不用关了
            if(listen_sock >= 0) close(listen_sock);
        }
};

// 静态成员变量必须在类外初始化
TcpServer* TcpServer::svr = nullptr;


